package run.halo.app.content;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

import java.time.Duration;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.retry.RetryException;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.retry.Retry;
import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.SchemeManager;

/**
 * Tests for {@link CategoryPostCountUpdater}.
 *
 * @author guqing
 * @since 2.15.0
 */
@SpringBootTest
class CategoryPostCountUpdaterTest {


    private final List<Post> storedPosts = posts();

    private final List<Category> storedCategories = categories();

    @Autowired
    SchemeManager schemeManager;

    @MockitoSpyBean
    ExtensionClient client;

    @Autowired
    ReactiveExtensionClient reactiveClient;

    private CategoryPostCountUpdater.CategoryPostCountService categoryPostCountService;

    Mono<Extension> deleteImmediately(Extension extension) {
        var name = extension.getMetadata().getName();
        var scheme = schemeManager.get(extension.getClass());
        return reactiveClient.fetch(scheme.type(), name)
            .flatMap(reactiveClient::delete)
            .flatMap(deleting -> reactiveClient.fetch(scheme.type(), name)
                .flatMap(e -> Mono.error(new RetryException("Extension still exists")))
                .retryWhen(Retry.backoff(10, Duration.ofMillis(100))
                    .filter(RetryException.class::isInstance)
                )
                .thenReturn(deleting)
            );
    }

    @BeforeEach
    void setUp() {
        categoryPostCountService =
            new CategoryPostCountUpdater.CategoryPostCountService(client);
        Flux.fromIterable(storedPosts)
            .flatMap(post -> reactiveClient.create(post))
            .as(StepVerifier::create)
            .expectNextCount(storedPosts.size())
            .verifyComplete();

        Flux.fromIterable(storedCategories)
            .flatMap(category -> reactiveClient.create(category))
            .as(StepVerifier::create)
            .expectNextCount(storedCategories.size())
            .verifyComplete();
    }

    @AfterEach
    void tearDown() {
        Flux.fromIterable(storedPosts)
            .flatMap(this::deleteImmediately)
            .as(StepVerifier::create)
            .expectNextCount(storedPosts.size())
            .verifyComplete();

        Flux.fromIterable(storedCategories)
            .flatMap(this::deleteImmediately)
            .as(StepVerifier::create)
            .expectNextCount(storedCategories.size())
            .verifyComplete();
    }

    @Test
    void reconcileStatusPostForCategoryA() {
        categoryPostCountService.recalculatePostCount(Set.of("category-A"));

        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
        verify(client).update(captor.capture());
        var value = captor.getValue();
        assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(1);
        assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
    }

    @Test
    void reconcileStatusPostForCategoryB() {
        categoryPostCountService.recalculatePostCount(Set.of("category-B"));

        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
        verify(client).update(captor.capture());
        var category = captor.getValue();
        assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(1);
        assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
    }

    @Test
    void reconcileStatusPostForCategoryC() {
        categoryPostCountService.recalculatePostCount(Set.of("category-C"));

        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
        verify(client).update(captor.capture());
        var value = captor.getValue();
        assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(2);
        assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
    }

    @Test
    void reconcileStatusPostForCategoryD() {
        categoryPostCountService.recalculatePostCount(Set.of("category-D"));

        ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
        verify(client).update(captor.capture());
        var value = captor.getValue();
        assertThat(value.getStatusOrDefault().postCount).isEqualTo(1);
        assertThat(value.getStatusOrDefault().visiblePostCount).isEqualTo(0);
    }

    private List<Category> categories() {
        /*
         * |-A(post-4)
         *   |-B(post-3)
         *   |-|-C(post-2,post-1)
         *   |-D(post-1)
         */
        Category categoryA = category("category-A");
        categoryA.getSpec().setChildren(List.of("category-B", "category-D"));

        Category categoryB = category("category-B");
        categoryB.getSpec().setChildren(List.of("category-C"));

        Category categoryC = category("category-C");
        Category categoryD = category("category-D");
        return List.of(categoryA, categoryB, categoryC, categoryD);
    }

    private Category category(String name) {
        Category category = new Category();
        Metadata metadata = new Metadata();
        metadata.setName(name);
        category.setMetadata(metadata);
        category.setSpec(new Category.CategorySpec());
        category.setStatus(new Category.CategoryStatus());

        category.getSpec().setDisplayName("display-name");
        category.getSpec().setSlug("slug");
        category.getSpec().setPriority(0);
        return category;
    }

    private List<Post> posts() {
        /*
         * |-A(post-4)
         *   |-B(post-3)
         *   |-|-C(post-2,post-1)
         *   |-D(post-1)
         */
        Post post1 = fakePost();
        post1.getMetadata().setName("post-1");
        post1.getSpec().setCategories(List.of("category-D", "category-C"));
        post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);

        Post post2 = fakePost();
        post2.getMetadata().setName("post-2");
        post2.getSpec().setCategories(List.of("category-C"));
        post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);

        Post post3 = fakePost();
        post3.getMetadata().setName("post-3");
        post3.getSpec().setCategories(List.of("category-B"));
        post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);

        Post post4 = fakePost();
        post4.getMetadata().setName("post-4");
        post4.getSpec().setCategories(List.of("category-A"));
        post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
        return List.of(post1, post2, post3, post4);
    }

    Post fakePost() {
        var post = TestPost.postV1();
        post.getSpec().setAllowComment(true);
        post.getSpec().setDeleted(false);
        post.getSpec().setExcerpt(new Post.Excerpt());
        post.getSpec().getExcerpt().setAutoGenerate(false);
        post.getSpec().setPinned(false);
        post.getSpec().setPriority(0);
        post.getSpec().setPublish(false);
        post.getSpec().setSlug("fake-post");
        return post;
    }

}